/*******************************************************************************
 * Copyright (c) 2008-2010 Cedric Vidal
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     Cedric Vidal - original idea, initial contribution and API
 *******************************************************************************/

package org.trans4j.spring;

import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import org.springframework.aop.framework.AopInfrastructureBean;
import org.springframework.beans.BeansException;
import org.springframework.beans.PropertyValues;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessor;
import org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory;
import org.springframework.beans.factory.support.DefaultSingletonBeanRegistry;
import org.trans4j.Trans4j;

/**
 * 
 * @author <a href="mailto:c.vidal@proxiad.com">Cedric Vidal, ProxiAD Group</a>
 * @see #setUseCircularReferencesWorkaround(boolean)
 */
public abstract class AbstractTrans4jAutoProxyCreator implements
		InstantiationAwareBeanPostProcessor, BeanClassLoaderAware,
		BeanFactoryAware, AopInfrastructureBean {

	private static final String ADD_SINGLETON_FACTORY_METHOD_NAME = "addSingletonFactory";
	private ClassLoader classLoader;
	private BeanFactory beanFactory;

	/**
	 * @return
	 */
	public boolean isUseCircularReferencesWorkaround() {
		return useCircularReferencesWorkaround;
	}

	/**
	 * Set to <code>true</code> to be able to inject mappers which have circular
	 * dependencies between them. This is a workaround because it requires
	 * calling a protected method of the Spring API. This is therefore not
	 * supported when a SecurityManager is active. Set to <code>false</code> to
	 * be able to use Trans4j when a SecurityManager is active, but disables
	 * circular dependencies between mappers. Defaults to <code>true</code>.
	 * 
	 * @param useCircularReferencesWorkaround
	 */
	public void setUseCircularReferencesWorkaround(
			boolean useCircularReferencesWorkaround) {
		this.useCircularReferencesWorkaround = useCircularReferencesWorkaround;
	}

	private boolean useCircularReferencesWorkaround = true;

	@Override
	public boolean postProcessAfterInstantiation(Object bean, String beanName)
			throws BeansException {
		return true;
	}

	@Override
	public Object postProcessBeforeInstantiation(Class<?> beanClass,
			final String beanName) throws BeansException {

		Object bean = null;
		if (isMapper(beanClass, beanName)) {
			bean = createProxy(beanClass);

			if (isUseCircularReferencesWorkaround()) {
				registerFactoryForCircularReferences(beanName, bean);
			}

			populate(bean, beanName);
		}

		return bean;
	}

	protected void registerFactoryForCircularReferences(final String beanName,
			final Object bean) {
		try {
			Class<DefaultSingletonBeanRegistry> clazz = DefaultSingletonBeanRegistry.class;
			Method[] methods = clazz.getMethods();
			Method addSingletonMethod = clazz.getDeclaredMethod(
					ADD_SINGLETON_FACTORY_METHOD_NAME, String.class,
					ObjectFactory.class);
			addSingletonMethod.setAccessible(true);
			addSingletonMethod.invoke(beanFactory, beanName,
					new ObjectFactory() {
						public Object getObject() throws BeansException {
							return bean;
						}
					});
		} catch (SecurityException e) {
			throw new RuntimeException(
					"Cannot use circular references workaround with the current security manager",
					e);
		} catch (NoSuchMethodException e) {
			throw new RuntimeException(
					"Cannot use circular references workaround with this version of Spring",
					e);
		} catch (IllegalArgumentException e) {
			throw new RuntimeException(
					"Cannot use circular references workaround with this version of Spring",
					e);
		} catch (IllegalAccessException e) {
			throw new RuntimeException(
					"Cannot access the method of the bean factory which adds singleton factories",
					e);
		} catch (InvocationTargetException e) {
			throw new RuntimeException(
					"An exception occured while adding singleton factory for bean '"
							+ beanName + "'", e);
		}
	}

	protected abstract boolean isMapper(Class<?> beanClass, String beanName);

	protected Object createProxy(Class<?> beanClass) {
		return Trans4j.mapper(beanClass, classLoader);
	}

	protected void populate(Object bean, String beanName) {
		if (beanFactory instanceof AbstractAutowireCapableBeanFactory) {
			AbstractAutowireCapableBeanFactory cbf = (AbstractAutowireCapableBeanFactory) beanFactory;
			cbf.configureBean(bean, beanName);
		}
	}

	@Override
	public PropertyValues postProcessPropertyValues(PropertyValues pvs,
			PropertyDescriptor[] pds, Object bean, String beanName)
			throws BeansException {
		return pvs;
	}

	@Override
	public Object postProcessAfterInitialization(Object bean, String beanName)
			throws BeansException {
		return bean;
	}

	@Override
	public Object postProcessBeforeInitialization(Object bean, String beanName)
			throws BeansException {
		return bean;
	}

	@Override
	public void setBeanClassLoader(ClassLoader classLoader) {
		this.classLoader = classLoader;
	}

	@Override
	public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
		this.beanFactory = beanFactory;
	}
	public BeanFactory getBeanFactory() {
		return beanFactory;
	}

}
